跳到主要内容

Vue3 中的 reactive 和 ref 的使用

ref 的使用

ref 的作用就是将一个原始数据类型(primitive data type)转换成一个带有响应式特性(reactivity)的数据类型,原始数据类型共有7个,分别是:

  • String
  • Number
  • Boolean
  • Null
  • Undefined
  • Symbol:它用来生成一个独一无二的值,它 Symbol 数据常用来给对象属性赋值,让对象属性具备唯一性,不容易被覆盖。
  • BigInt : 解决精度缺失的问题,提供了一种方法来表示大于 2^53-1 的整数。BigInt 可以表示任意大的整数

相比于 Vue2,用 ref 的好处就是传值时可以不用再写 this

<template>
<img alt="Vue logo" src="./assets/logo.png" />
<h1>{{ title }}</h1>
<button @click="handleClick"></button>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

export default defineComponent({
name: "App",
setup() {
const title = ref("你好, Vue3!");
const handleClick = () => {
title.value = "数据来了";
};
return { title, handleClick };
},
});
</script>

reactive 的使用

Vue3 提供了一个方法:reactive (等价于 Vue2 中的 Vue.observable() )来赋予对象(Object) 响应式的特性,那么我们可以将上述代码用对象的形式改写为:

<template>
<img alt="Vue logo" src="./assets/logo.png" />
<h1>{{ data.title }}</h1>
<button @click="data.handleClick"></button>
</template>

<script lang="ts">
import { defineComponent, reactive } from "vue";

export default defineComponent({
name: "App",
setup() {
const data = reactive({
title: "你好, Vue3",
handleClick: () => {
data.title = "数据来了";
},
});
return { data };
},
});
</script>

你可能会觉得 data.xxx 的写法太麻烦,那么我们可以使用 es6 新语法扩展运算符来简化一下:

<template>
<img alt="Vue logo" src="./assets/logo.png" />
<h1>{{ title }}</h1>
<button @click="handleClick"></button>
</template>

<script lang="ts">
import { defineComponent, reactive } from "vue";

export default defineComponent({
name: "App",
setup() {
const data = reactive({
title: "你好, Vue3",
handleClick: () => {
data.title = "数据来了";
},
});
return { ...data };
},
});
</script>

Bug 出现

不出意外,你会发现这个简化后的代码竟然无效,不管怎么点按钮,页面并没有发生变化!事实上,这样写没有效果的原因就在于一个响应型对象(reactive object) 一旦被销毁或展开(如上面代码那样),其响应式特性(reactivity)就会丢失。

通过类型检查我们可以知道,虽然 data.title 的值确实发生了变化,但 data.title 的类型只是一个普通的字符串(String) ,并不具有响应式特性(reactivity),故而页面也没有随着其值的变化而重新渲染。

toRefs 响应型变普通型

为了解决上述问题,Vue3 又提供了一个新的 API:toRefs,它可以将一个响应型对象(reactive object)转化为普通对象(plain object),同时又把该对象中的每一个属性转化成对应的响应式属性(ref)。

说白了就是放弃该对象(Object)本身的响应式特性(reactivity),转而给对象里的属性赋予响应式特性(reactivity)。故而我们可以将代码修复成下面这样:

<template>
<img alt="Vue logo" src="./assets/logo.png" />
<h1>{{ title }}</h1>
<button @click="handleClick"></button>
</template>

<script lang="ts">
import { defineComponent, reactive, toRefs } from "vue";

export default defineComponent({
name: "App",
setup() {
const data = reactive({
title: "你好, Vue3",
handleClick: () => {
data.title = "数据来了";
},
});
const dataAsRefs = toRefs(data);
/*
Type of dataAsRefs:
{
title: Ref<string>,
handleClick: Ref<() => void>
}
*/
return { ...dataAsRefs };
},
});
</script>

ref 和 reactive 的使用区别

refreactive 一个针对原始数据类型,而另一个用于对象,这两个 API 都是为了给 JavaScript 普通的数据类型赋予响应式特性(reactivity)。

根据 Vue3 官方文档,这两者的主要区别在于每个人写 JavaScript 时的风格不同,有人喜欢用原始数据类型(primitives),把变量单独拎出来写;而有人喜欢用对象(Object),把变量当作对象里的属性,都写在一个对象里头,比如:

// style 1: separate variables
let x = 0
let y = 0

function updatePosition(e) {
x = e.pageX
y = e.pageY
}

// --- compared to ---

// style 2: single object
const pos = {
x: 0,
y: 0
}

function updatePosition(e) {
pos.x = e.pageX
pos.y = e.pageY
}

使用 ref 还是 reactive 可以选择这样的准则

第一,就像刚才的原生 javascript 的代码一样,像你平常写普通的 js 代码选择原始类型和对象类型一样来选择是使用 ref 还是 reactive。 第二,所有场景都使用 reactive,但是要记得使用 toRefs 保证 reactive 对象属性保持响应性。

通过 ref 取得 DOM 对象

获取单个节点比较简单

<template>
<div ref="refDiv">dddd</div>
</template>
<script>
import {ref, onMounted} from "vue";
export default{
setup(){
const refDiv = ref(null);

onMounted(()=>{
console.log(refDiv)
})
// 不要忘了在return中添加refDiv
return {refDiv};
}
}
</script>

注意!! 别忘了在 return 里面把这个 refDiv 返回出去

Vue 3.0获取多个 DOM(一般用于获取数组)

<template>
<div>获取多个DOM元素</div>
<ul>
<li v-for="(item, index) in arr" :key="index" :ref="setRef">
{{ item }}
</li>
</ul>
</template>

<script>
import { ref, nextTick } from 'vue';

export default {
setup() {
const arr = ref([1, 2, 3]);
// 存储dom数组
const myRef = ref([]);

const setRef = (el) => {
myRef.value.push(el);
};

nextTick(() => {
console.dir(myRef.value);
});
return {
arr,
setRef
};
}
};
</script>

注:console.dir() 可以显示一个对象所有的属性和方法

toRef 引用对象属性

toRef 是将某个对象中的某个值转化为响应式数据(不更新 UI),其接收两个参数,第一个参数为 obj 对象;第二个参数为对象中的属性名

使用 toRef 将某个对象中的属性变成响应式数据,修改响应式数据是 会影响到原始数据 的。但是需要注意,如果修改通过 toRef 创建的响应式数据,并不会触发 UI 界面的更新。

代码如下:

// 1. 导入 toRef
import {toRef} from 'vue'
export default {
setup() {
const obj = {count: 3}
// 2. 将 obj 对象中属性count的值转化为响应式数据
const state = toRef(obj, 'count')

// 3. 将toRef包装过的数据对象返回供template使用
return {state}
}
}

但其实表面上看上去 toRef 这个 API 好像非常的没用,因为这个功能也可以用 ref 实现,代码如下

// 1. 导入 ref
import {ref} from 'vue'
export default {
setup() {
const obj = {count: 3}
// 2. 将 obj 对象中属性count的值转化为响应式数据
const state = ref(obj.count)

// 3. 将ref包装过的数据对象返回供template使用
return {state}
}
}

乍一看好像还真是,其实这两者是有区别的,我们可以通过一个案例来比较一下,代码如下

<template>
<p>{{ state1 }}</p>
<button @click="add1">增加</button>

<p>{{ state2 }}</p>
<button @click="add2">增加</button>
</template>

<script>
import {ref, toRef} from 'vue'
export default {
setup() {
const obj = {count: 3}
const state1 = ref(obj.count)
const state2 = toRef(obj, 'count')

function add1() {
state1.value ++
console.log('原始值:', obj);
console.log('响应式数据对象:', state1);
}

function add2() {
state2.value ++
console.log('原始值:', obj);
console.log('响应式数据对象:', state2);
}

return {state1, state2, add1, add2}
}
}
</script>

我们分别用 reftoRef 将 obj 中的 count 转化为响应式,并声明了两个方法分别使 count 值增加,每次增加后打印一下原始值 obj 和被包装过的响应式数据对象,同时还要看看视图的变化

ref:

可以看到,在对响应式数据的值进行 +1 操作后,视图改变了,原始值未改变,响应式数据对象的值也改变了,这说明 ref 是对原数据的一个拷贝,不会影响到原始值,同时响应式数据对象值改变后会同步更新视图

toRef:

可以看到,在对响应式数据的值进行 +1 操作后,视图未发生改变,原始值改变了,响应式数据对象的值也改变了,这说明 toRef 是对原数据的一个引用,会影响到原始值,但是响应式数据对象值改变后会不会更新视图

总结:

ref 是对传入数据的拷贝;toRef 是对传入数据的引用 ref 的值改变会更新视图;toRef 的值改变不会更新视图

如果要更新数据,得使用 computed

shallowReactive 浅层响应工具

听这个API的名称就知道,这是一个浅层的 reactive,难道意思就是原本的 reactive 是深层的呗,没错,这是一个用于性能优化的API

其实将 obj 作为参数传递给 reactive 生成响应式数据对象时,若 obj 的层级不止一层,那么会将每一层都用 Proxy 包装一次,我们来验证一下

import {reactive} from 'vue'
export default {
setup() {
const obj = {
a: 1,
first: {
b: 2,
second: {
c: 3
}
}
}

const state = reactive(obj)

console.log(state)
console.log(state.first)
console.log(state.first.second)
}
}

来看一下打印结果:

设想一下如果一个对象层级比较深,那么每一层都用 Proxy 包装后,对于性能是非常不友好的

接下来我们再来看看 shallowReactive

import {shallowReactive} from 'vue'
export default {
setup() {
const obj = {
a: 1,
first: {
b: 2,
second: {
c: 3
}
}
}

const state = shallowReactive(obj)

console.log(state)
console.log(state.first)
console.log(state.first.second)
}
}

结果非常的明了了,只有第一层被 Proxy 处理了,也就是说只有修改第一层的值时,才会响应式更新

shallowRef 手动响应数据

这是一个浅层的 ref,与 shallowReactive 一样是拿来做性能优化的,这个 shallowReactive 主要配合 triggerRef 来进行手动更新响应数据

shallowReactive 是监听对象第一层的数据变化用于驱动视图更新,那么 shallowRef 则是监听 .value 的值的变化来更新视图的

我们来看一下具体代码

<template>
<p>{{ state.a }}</p>
<p>{{ state.first.b }}</p>
<p>{{ state.first.second.c }}</p>
<button @click="change1">改变1</button>
<button @click="change2">改变2</button>
</template>

<script>
import {shallowRef} from 'vue'
export default {
setup() {
const obj = {
a: 1,
first: {
b: 2,
second: {
c: 3
}
}
}

const state = shallowRef(obj)
console.log(state);

function change1() {
// 直接将state.value重新赋值
state.value = {
a: 7,
first: {
b: 8,
second: {
c: 9
}
}
}
}

function change2() {
state.value.first.b = 8
state.value.first.second.c = 9
console.log(state);
}

return {state, change1, change2}
}
}
</script>

首先看一下被 shallowRef 包装过后是怎样的结构

然后再来看看改变其值会有什么变化

我们先点击了第二个按钮,发现数据确实被改变了,但是视图并没随之更新;

于是点击了第一个按钮,即将整个 .value 重新赋值了,视图就立马更新了

这么一看,未免也太过麻烦了,改个数据还要重新赋值,不要担心,此时我们可以用到另一个API,叫做 triggerRef ,调用它就可以立马更新视图,其接收一个参数 state ,即需要更新的 ref 对象

我们来使用一下

<template>
<p>{{ state.a }}</p>
<p>{{ state.first.b }}</p>
<p>{{ state.first.second.c }}</p>
<button @click="change">改变</button>
</template>

<script>
import {shallowRef, triggerRef} from 'vue'
export default {
setup() {
const obj = {
a: 1,
first: {
b: 2,
second: {
c: 3
}
}
}

const state = shallowRef(obj)
console.log(state);

function change() {
state.value.first.b = 8
state.value.first.second.c = 9
// 修改值后立即驱动视图更新
triggerRef(state)
console.log(state);
}

return {state, change}
}
}
</script>

我们来看一下具体过程

可以看到,我们没有给 .value 重新赋值,只是在修改值后,调用了 triggerRef 就实现了视图的更新

Reference

Vue 3.0 ref 和reactive 区别 vue 3.0 使用ref获取dom元素